package com.gframework.util;

import java.math.BigDecimal;
import java.math.RoundingMode;

/**
 * 数学计算工具类.
 * <ul>
 * <li><strong>yoy {@code -} </strong>计算同/环比</li>
 * <li><strong>round {@code -} </strong>精准四舍五入</li>
 * <li><strong>roundBanker {@code -} </strong>银行家四舍五入</li>
 * <li><strong>gcd {@code -} </strong>求最大公约数</li>
 * <li><strong>lcm {@code -} </strong>求最小公倍数</li>
 * </ul>
 * 
 * @since 1.0.0
 * @author Ghwolf
 */
public class MathUtils {
	private MathUtils(){}
	
	/**
	 * 计算同/环比.
	 * <p>同比通常指的是年同比，环比通常指的是月环比，但也不限制你可以计算其他时间区间范围。
	 * <p>同比环比的计算公式是一样的，都是当前一段时间范围内与同期一段范围内的一个计算结果。
	 * <p>计算公式：（本期数－同期数）÷同期数×100%
	 * @param currentAmount 本期数
	 * @param samePeriodAmount 同期数
	 * @return 返回计算结果(计算结果并没有按照百分符的表示形式去×100，仅保留比值计算结果)
	 */
	public static double yoy(double currentAmount,double samePeriodAmount) {
		return (currentAmount - samePeriodAmount) / samePeriodAmount;
	}
	
	/**
	 * 精准四舍五入.
	 * <p>由于不同的语言中，对四舍五入尤其是负数上有着较为明显的实现差异，使用起来非常不方便。
	 * <p>而数学中标准的四舍五入可以通过坐标轴来进行判断，以横坐标为例，看是靠近左边还是右边，就向那边进位。如果刚好在中间，例如1.5，则进位，例如：
	 * <ul>
	 * <li>round(1.5) = 2</li>
	 * <li>round(-1.5) = 1</li>
	 * </ul>
	 * <p>当然你也可以通过另外一种方式进行理解，将这个数加上0.5，然后进行floor处理。
	 * <p><strong>在java中，{@link Math#round(double)}就是这么实现的</strong>。这是合理的，但是他并不支持保留指定小数位的功能。
	 * 通常都会使用{@link BigDecimal}来运算，但是如果用不好{@link RoundingMode}，会造成极大的负数运算误差。
	 * <p>此方法解决了这个问题，你可以放心的进行四舍五入运算，并且此方法的进退位逻辑与Math类的round方法是一样的。
	 * @param value 要四舍五入的小数
	 * @param scale 要保留的小数位，如果小于0，则按照0处理
	 * @return 返回四舍五入后的小数
	 * @see Math#round(double)
	 * @see RoundingMode#HALF_UP
	 * @see RoundingMode#HALF_DOWN
	 */
	public static double round(double value,int scale) {
		return BigDecimal.valueOf(value).divide(BigDecimal.ONE,scale,value >= 0 ? RoundingMode.HALF_UP : RoundingMode.HALF_DOWN).doubleValue();
	}
	/**
	 * 银行家四舍五入.
	 * <p>这是一个用于金融计算的四舍五入算法，其具体含义如下：
	 * <strong>4舍6入，5看前（看上一位），偶数退位基数进位</strong>
	 * @param value 要进行四舍五入的数字
	 * @param scale 保留的小数位
	 * @return 返回银行家四舍五入后的小数
	 */
	public static double roundBanker(double value,int scale) {
		return BigDecimal.valueOf(value).divide(BigDecimal.ONE,scale,RoundingMode.HALF_EVEN).doubleValue();
	}

	/**
	 * 求两个数得 <strong>最大公约数</strong>.<br>
	 * 利用欧几里德的辗转相除算法。
	 * 
	 * <pre>
	 * 1、对于已知的两自然数m、n，假设m>n；
	 * 2、计算m除以n，将得到的余数记为r；
	 * 3、如果r=0，则n为求得得最大公约数，否则执行下面一步；
	 * 4、将n得值保存到m中，将r得值保存到n中，重复执行（2）和（3），直到r=0
	 * </pre>
	 * 
	 * @param a 第一个数
	 * @param b 第二个数
	 * @return 返回最大公约数
	 */
	public static int gcd(int a, int b) {
		// 递归操作
		// return b == 0 ? a : gcd(b,a % b);
		while (b != 0) {
			int i = b;
			b = a % b;
			a = i;
		}
		return a;
	}

	/**
	 * 求两个数得 <strong>最小公倍数</strong>.<br>
	 * 
	 * @param a 第一个数
	 * @param b 第二个数
	 * @return 返回最小公倍数
	 */
	public static int lcm(int a, int b) {
		return a / gcd(a, b) * b;
	}

}
